/* ***** BEGIN LICENSE BLOCK *****
 *   Version: GPL 2.0
 *
 * Bookstack extension: a queue implementation for bookmarks.
 * Copyright (C) 2007-2011  Adam Dane
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * ***** END LICENSE BLOCK ***** */

/*
 * The structure of the stack is a regular bookmark folder containing:
 * -Bookmarks
 * -Folders - can be stack, opening pulls contents into parent
 * -Separators - for display only
 * -Livemarks - Treated like folders for opening, but can't be a stack.
 */

"use strict";

var bookstack,
    EXPORTED_SYMBOLS = ["bookstack"];

if (typeof bookstack === "undefined") {
  bookstack = {};
}

(function () {
  var Cu = Components.utils,
      Ci = Components.interfaces,
      Cr = Components.results,
      BMSVC;
  if (typeof PlacesUtils === "undefined") {
    try {
      Cu["import"]("resource://gre/modules/PlacesUtils.jsm");
    }
    catch (exPlacesUtils) {}
  }
  try {
    Cu["import"]("resource://bookstack/bookstack-Preferences.jsm");
  }
  catch (exBookstack) {}

  BMSVC = bookstack.serv.BMSVC();
  bookstack.stack = {
    windows: [],

    addWindow: function (aWindow) {
      if (this.windows.indexOf(aWindow) === -1) {
        this.windows.push(aWindow);
      }
    },

    getStringBundle: function () {
      return bookstack.serv.getWindow().document.getElementById(
        "bookstack-strings");
    },

    getString: function (aStringName) {
      return this.getStringBundle().getString(aStringName);
    },

    doesFolderExist: function (aFolderId) {
      var title;
      try {
        aFolderId = parseInt(aFolderId, 10);
      }
      catch (exDoesFolderExistParseInt) {
        return false;
      }
      try {
        title = BMSVC.getItemTitle(aFolderId);
        if (typeof title !== "undefined") {
          return true;
        }
      }
      catch (exDoesFolderExistGetTitle) {}
      return false;
    },

    // Generic mechanism for creating either a folder in batch mode
    setupFolderInBatch: function (aParent, aFolderName, aIndex) {
      var closure = {
            parent: aParent,
            folderName: aFolderName,
            index: aIndex,
            id: null
          },
          doBatch;
      closure.wrappedJSObject = closure;
      doBatch = {
        runBatched: function (aUserData) {
          var closed = aUserData.wrappedJSObject;
          closed.id = BMSVC.createFolder(
            closed.parent, closed.folderName, closed.index);
        }
      };
      try {
        BMSVC.runInBatchMode(doBatch, closure);
      }
      catch (ex) {}
      return closure.wrappedJSObject.id;
    },

    // maintenance of stack
    setupStackFolder: function () {
      var id;
      this.unwatchBookmarks();
      try {
        id = this.setupFolderInBatch(
          BMSVC.bookmarksMenuFolder,
          this.getString("stackname"),
          BMSVC.DEFAULT_INDEX);
      }
      catch (ex) {}
      this.watchBookmarks();
      bookstack.pref.STACK_FOLDER.persist(id);
      return id;
    },

    // Workaround for if the sessionstore-windows-restored never hits.
    // That was Bug 645428, so in theory this is removable after ?(Fx 8)?
    ensureLoaded: function () {
      var bookstackOverlay = bookstack.serv.getWindow().bookstackOverlay;
      if (bookstackOverlay && !bookstackOverlay.loaded) {
        bookstackOverlay.onLoad();
      }
    },

    // Simple getter for the current stack folder, creates if doesn't exist.
    getShelf: function () {
      this.ensureLoaded();
      var stack = bookstack.pref.STACK_FOLDER.value();
      if (!stack || !this.doesFolderExist(stack)) {
        return this.setupStackFolder();
      }
      return stack;
    },

    // Keeps resetting of folders from causing excessive recursion:
    //  We keep track of all of the extension's BMOs here.
    //  We enable or disable them in a group (though facilities remain
    //  for individual changes too).
    BMObservers: [],
    BMObserversDisabled: false,
    BMObserverWatchState: 0,

    registerBMO: function (aBMO) {
      var observers = this.BMObservers,
          observer = observers.indexOf(aBMO);
      if (observer === -1) { /* Ensure it's not there already */
        this.enableObserver(aBMO);
        observers.push(aBMO);
        return aBMO;
      }
      return null;
    },

    unregisterBMO: function (aBMO) {
      var observers = this.BMObservers,
          observer = observers.indexOf(aBMO);
      if (observer > -1) {
        this.disableObserver(observers[observer]);
        return observers.splice(observer, 1);
      }
      return null;
    },

    disableObserver: function (aObserver) {
      if (!this.BMObserversDisabled) {
        try {
          BMSVC.removeObserver(aObserver);
        }
        catch (ex) {}
      }
    },

    enableObserver: function (aObserver) {
      if (!this.BMObserversDisabled) {
        try {
          BMSVC.addObserver(aObserver, false);
        }
        catch (ex) {}
      }
    },

    // We track the number of observers that have enabled/disabled watching
    watchBookmarks: function () {
      this.BMObserverWatchState -= 1;
      if (this.BMObserverWatchState === 0) {
        var observers = this.BMObservers;
        this.BMObserversDisabled = false;
        observers.forEach(this.enableObserver, this);
      }
    },

    // We track the number of observers that have enabled/disabled watching
    unwatchBookmarks: function () {
      if (this.BMObserverWatchState === 0) {
        var observers = this.BMObservers;
        this.BMObserversDisabled = false;
        observers.forEach(this.disableObserver, this);
        this.BMObserversDisabled = true;
      }
      this.BMObserverWatchState += 1;
    },

    // Instead of trying to call for a note everywhere a change is made, this
    // allows us to sit back, maybe watch a little Mork and Mindy on channel 57.
    notificationBMObserver: {
      onBeginUpdateBatch: function () {},
      onEndUpdateBatch: function () {},
      onItemChanged: function (aId, aProp, aB_AnnoP, aValue, aMod, aType,
                               aParentId, aGUID, aParentGUID) {},
      onItemVisited: function (aId, aVisitId, aTime, aTransitionType, aURI,
                               aParentId, aGUID, aParentGUID) {},

      onItemAdded: function (aId, aParentId, aIndex, aType, aURI, aTitle, aDate,
                             aGUID, aParentGUID) {
        if (aParentId === bookstack.stack.getShelf()) {
          bookstack.stack.itemAddedNotification(aId, aURI, aTitle);
        }
      },

      onItemRemoved: function (aId, aParentId, aIndex, aType, aURI, aGUID,
                               aParentGUID) {
        var NSVC = bookstack.stack.noteService,
            matched = -1;
        NSVC.notifications.some(function (aNoteSet, aIndex) {
          if (!aNoteSet.length)
            return false;
          var noteId = parseInt(aNoteSet[0].note.id.split('-')[2], 10);
          if (aId === noteId) {
            matched = aIndex;
            return true;
          }
          return false;
        });
        if (matched > -1) {
          NSVC.removeNote({ value: matched });
        }
      },

      onItemMoved: function (aId, aOldParentId, aOldIdx, aNewParentId, aNewIdx,
                             aType, aGUID, aOldParentGUID, aNewParentGUID) {
        var shelf = bookstack.stack.getShelf();
        if (aNewPId === shelf && aOldParentId !== shelf) {
          bookstack.stack.itemAddedNotification(
            aId, BMSVC.getBookmarkURI(aId), BMSVC.getItemTitle(aId));
        }
      }
    },

  // Uses the doorhang-type notifications (Fx4+)
    noteService: {
      // A list of the unshown, new bookmarks.
      notifications: [],
      notificationTimeout: null,

      // Adds a note and makes sure the alerts have been kicked off.
      ping: function (aNote) {
        var NSVC = this,
            notes = NSVC.notifications;
        NSVC.popupNotification(aNote);
        notes.push(aNote);
//        NSVC.update();
      },

      // Observes for when we're ready to show another note (if there is one).
      handler: function () {
        var that = this;
        function observer(subj, topic, id) {
          var NSVC = that,
              notes = NSVC.notifications,
              browser,
              note;
          if (topic === "alertclickcallback") {
            NSVC.hasNote = false;
            browser = bookstack.serv.getWindow();
            // The user may be trying to edit a defunct item.
            try {
              BMSVC.getItemTitle(parseInt(id, 10));
              browser.bookstackOverlay.properties(id, 'bookmark');
            }
            catch (exHandler) {
            }
          }
          else {
            if (notes.length > 0) {
              note = notes.shift();
              // The user may have deleted or viewed an item before its
              // note got to the front of the line.
              try {
                BMSVC.getItemTitle(parseInt(note.id, 10));
                NSVC.nativeNotification(note);
                NSVC.hasNote = true;
              }
              catch (exHandlerDoNote) {
                NSVC.hasNote = false;
                NSVC.update();
              }
            }
            else {
              NSVC.hasNote = false;
            }
          }
        }
        return { observe: observer };
      },

      // Kicks off the alerts if they aren't already going.
      update: function () {
        var notes = this.notifications;
        if (this.hasNote || notes.length < 1) {
          return;
        }
        this.nativeNotification(notes.shift());
        this.hasNote = true;
      },

      // The actual alert code, will display native notifications from Fx3.6+
      nativeNotification: function (note) {
        var strBundle = bookstack.stack.getStringBundle(),
            title = strBundle.getString("bookstack-title"),
            bookShelf = bookstack.stack.getShelf(),
            parentFolder;
        try {
          parentFolder = BMSVC.getFolderIdForItem(note.id);
        }
        catch (ex) {}
        if (!parentFolder || parentFolder !== bookShelf) {
          this.hasNote = false;
          this.update();
        }
        else {
          bookstack.serv.ASVC().showAlertNotification(
            'chrome://bookstack-img/skin/bookstack-icon.png',
            title,
            note.label,
            true,
            note.id,
            this.handler(),
            "bookstack-" + note.id);
        }
      },

      noteCallback: function (aReason) {
/*        switch (aReason) {
          case "dismissed":
          case "removed":
          case "shown":
          default:
        }*/
      },

      popupNotification: function (aNote) {
        var NSVC = this,
            strBundle = bookstack.stack.getStringBundle(),
            buttonLabel = strBundle.getString("stack-editbutton"),
            buttonKey = strBundle.getString("stack-editkey"),
            options = {
              persistence: 1,
              timeout: Date.now() + 1000,
              persistWhileVisible: true,
              neverShow: true
            },
            noteSet = [],
            currentWin = bookstack.serv.getWindow();
        try {
          // Add note to all windows, avoids missing note due to adding from a
          // blurred window (normally would show when focused, but we remove
          // after the timeout)
          bookstack.stack.windows.forEach(function (aWindow) {
            noteSet.unshift({
              note: aWindow.PopupNotifications.show(
                aWindow.getBrowser().selectedBrowser,
                "bookstack-notification-" + aNote.id,
                aNote.label,
                "bookstack-notification-icon",
                null,
                null,
                options),
              window: aWindow
            });
            // Show even if the windows aren't focused
            var anchor = aWindow.PopupNotifications.iconBox.querySelector(
              "#bookstack-notification-icon");
            aWindow.PopupNotifications._update(anchor);
          });
          if (!NSVC.notificationTimeout) {
            NSVC.notificationTimeout = bookstack.serv.getWindow().setTimeout(
              NSVC.removeNote, 3000);
          }
        }
        catch (ex) {}
        NSVC.notifications.push(noteSet);
        NSVC.feedback();
      },

      // aIndex is wrapped in an object (so, aIndex.value) because |setTimeout|
      // on Mozilla adds an extra parameter (how much it missed the mark by).
      removeNote: function (aIndex) {
        var NSVC = bookstack.stack.noteService;
        if (aIndex && aIndex.value) {
          NSVC.notifications.splice(
            aIndex.value, 1)[0].forEach(function(aNote) {
              aNote.note.remove();
            });
        }
        else {
          try {
            if (NSVC.notifications.length) {
              NSVC.notifications.pop().forEach(function (aNote) {
                aNote.note.remove();
              });
            }
          }
          catch (ex) {}
          if (NSVC.notifications.length) {
            NSVC.notificationTimeout = bookstack.serv.getWindow().setTimeout(
              NSVC.removeNote, 3000);
          }
          else {
            NSVC.notificationTimeout = null;
          }
        }
      },

      // Add the class to cause animation to fire, per window
      // Animation doesn't necessarily happen once per item added, but rather
      // once per action, so adding ten tabs via 'add all tabs' only fires once
      feedback: function (aElement) {
        var NSVC = this;
        bookstack.stack.windows.forEach(function (aWindow) {
          var element = aWindow.document.getElementById(
            "bookstack-notification-icon");
          if (element) {
            element.classList.add("bookstack-ping");
            element.addEventListener(
              "animationiteration", NSVC.cleanFeedback, false);
          }
        });
      },

      // Remove animation class after firing
      cleanFeedback: function (aEvent) {
        var NSVC = bookstack.stack.noteService;
        aEvent.target.classList.remove("bookstack-ping");
        aEvent.target.removeEventListener(
          "animationiteration", NSVC.cleanFeedback, false);
      }
    },

    // Wrapper to ping in a new note.
    itemAddedNotification: function (aId, aURI, aTitle) {
      if (bookstack.pref.STACK_NOTIFY.value()) {
        bookstack.stack.noteService.ping({
          label: aTitle || aURI,
          id: aId
        });
      }
    },

    // Add the specified URL as a new item in the stack.
    onAddToStackIndex: function (aInfo) {
      var shelf = this.getShelf(),
          bookmarkURI,
          newBookmark;
      if (!aInfo.uri && !aInfo.url) {
        return false;
      }
      bookmarkURI = aInfo.uri || bookstack.serv.getURI(aInfo.url);
      newBookmark = BMSVC.insertBookmark(
        shelf,
        bookmarkURI,
        aInfo.index || BMSVC.DEFAULT_INDEX,
        aInfo.title || bookmarkURI.spec);
      if (aInfo.edit) {
        bookstack.serv.getWindow().bookstackOverlay.properties(
          newBookmark, "bookmark");
      }
      return newBookmark;
    },

    // Returns true if the current stack is empty, otherwise returns false.
    // Empty means no regular bookmarks as direct children.
    checkEmpty: function (aFolder) {
      var folderNode,
          i,
          childNode,
          type,
          empty = true;
      if (!aFolder) {
        aFolder = this.getShelf();
      }
      folderNode = PlacesUtils.getFolderContents(aFolder, false, false).root;
      for (i = 0; empty && i < folderNode.childCount; i += 1) {
        childNode = folderNode.getChild(i);
        type = childNode.type;
        if (type === folderNode.RESULT_TYPE_URI) {
          empty = false;
        }
        else if (type === folderNode.RESULT_TYPE_FOLDER ||
                 type === folderNode.RESULT_TYPE_REMOTE_CONTAINER) {
          empty = this.checkEmpty(childNode.itemId);
        }
      }
      folderNode.containerOpen = false;
      return empty;
    },

    // Approximates the utilityOverlay.js behavior for function of same name.
    whereToOpenLink: function (aEvent, aIgnoreButton, aIgnoreAlt) {
      if (!aEvent) {
        return "current";
      }
      var shiftKey = aEvent.shiftKey,
          ctrlKey = aEvent.ctrlKey,
          metaKey = aEvent.metaKey,
          middle = !aIgnoreButton && aEvent.button === 1,
          middleTab = bookstack.serv.getBoolPref(
            "browser.tabs.opentabfor.middleclick");
      if (metaKey || ctrlKey || (middle && middleTab)) {
        return shiftKey ? "tabshifted" : "tab";
      }
      return "current";
    },

    // Returns the place to open for a simulated plain click event.
    getDefaultAction: function () {
      return this.whereToOpenLink(
        {
          shiftKey: false,
          ctrlKey: false,
          metaKey: false,
          altKey: false,
          button: 0
        }, false, false);
    },

    // Wraps popItem to view the first item in stack.
    // This triggers MOVE on the item if it's not REMOVEd.
    popFirst: function (aEvent) {
      var where = bookstack.stack.whereToOpenLink(aEvent, false, false),
          shelf = this.getShelf();
      if (this.checkEmpty(shelf)) {
        return false;
      }
      this.popItem(BMSVC.getIdForItemAt(shelf, 0), where, true);
      return true;
    },

    // Remove/view all the given (arr) items.
    multiPop: function (aItems, aWhere) {
      if (!aWhere) {
        aWhere = bookstack.stack.getDefaultAction();
      }
      var whereMore = "tab",
          closure = null,
          doBatch;

      if (aItems.length > 1 && aWhere === "current") {
        // Multiple items need to open in background, except for the first
        if (bookstack.serv.getBoolPref(
          "browser.tabs.loadBookmarksInBackground")) {
          aWhere = "tab";
          whereMore = "tabshifted";
        }
        else {
          aWhere = "tabshifted";
        }
      }

      closure = {
        items: aItems,
        where: aWhere,
        whereMore: whereMore
      };

      closure.wrappedJSObject = closure;
      doBatch = {
        runBatched: function (aUserData) {
          var closed = aUserData.wrappedJSObject;
          closed.items.forEach(function(aItem, aIndex) {
            var where = (aIndex > 0) ? closed.whereMore : closed.where;
            bookstack.stack.popItem(aItem, where);
          });
        }
      };
      try {
        BMSVC.runInBatchMode(doBatch, closure);
      }
      catch (ex) {}
    },

    // Open an item; |where| determines whether it should be in a new
    // tab or the current tab, based on the user's preference and action.
    popItem: function (aId, aWhere, aPop) {
      function makeCallback() {
        function callback(aBoolResult, aLivemark) {
          let ready = false;
          let uriSpec = "";
          let index = 0;
          let shelf = bookstack.stack.getShelf();
          ready = true;
          if (ready) {
            if (aBoolResult) {
              bookstack.stack.pushLivemarkFolder(aLivemark, aWhere, aPop);
              return;
            }
            else if (bookstack.stack.pushFolder(aId) === 0) {
              return;
            }
            BMSVC.moveItem(aId, shelf, BMSVC.DEFAULT_INDEX);
            let itemId = BMSVC.getIdForItemAt(shelf, 0);
            bookstack.stack.popItem(itemId, aWhere, aPop);
          }
        }
        return callback;
      }
      aId = parseInt(aId, 10);
      var shelf = bookstack.stack.getShelf(),
          index,
          callback,
          type = BMSVC.getItemType(aId);
      if (bookstack.stack.checkEmpty() ||
          BMSVC.getFolderIdForItem(aId) !== shelf) {
        return;
      }
      index = BMSVC.getItemIndex(aId);
      // separators are treated as their nextSibling's kid brother, so
      // nextSibling gets dragged along when you pop the separator
      if (type === BMSVC.TYPE_SEPARATOR) {
        BMSVC.removeItem(aId);
      }
      // Folders and livemarks get moved to the bottom of the stack, with
      // their children added to the top. That process is recursive.
      else if (type === BMSVC.TYPE_FOLDER ||
          type === BMSVC.TYPE_DYNAMIC_CONTAINER) {
        callback = makeCallback();
        bookstack.serv.getLivemark(aId, callback);
        return;
      }
      else {
        bookstack.stack.openThisIndex(aId, aWhere, aPop);
        return;
      }
      aId = BMSVC.getIdForItemAt(shelf, index);
      bookstack.stack.popItem(aId, aWhere, aPop);
    },

    // Given an id open that item. See whereToOpenLink for |where|, callback is
    // a function to style the item if the sidebar is open and is removing.
    // |aPop| says to move it to bottom if not removing.
    openThisIndex: function (aId, aWhere, aPop) {
      var uri,
          url,
          browser = bookstack.serv.getBrowser(),
          i = browser.mTabContainer.selectedIndex,
          tab,
          options = {
            inBackground: bookstack.serv.getBoolPref(
              "browser.tabs.loadBookmarksInBackground")
          };
      try {
        uri = BMSVC.getBookmarkURI(aId);
        url = uri.spec;
      }
      catch (exURI) {
        return false;
      }
      switch (aWhere) {
        case "tabshifted":
          options.inBackground = !options.inBackground;
          tab = browser.loadOneTab("about:blank", options);
          break;
        case "tab":
          tab = browser.loadOneTab("about:blank", options);
          break;
        default:
          tab = browser.mTabContainer.childNodes[i];
          break;
      }

      try {
        if (bookstack.pref.STACK_RITEMS.value()) {
          BMSVC.removeItem(aId);
          if (tab.linkedBrowser.currentURI.spec !== "about:blank") {
            tab.linkedBrowser.loadURI("about:blank");
          }
          // setTimeout to ensure the change is seen if the new tab is focused.
          bookstack.serv.getWindow().setTimeout(function () {
            tab.linkedBrowser.userTypedValue = url;
            tab.linkedBrowser.userTypedClear = 0;
            tab.linkedBrowser.docShell.setCurrentURI(uri);
            try {
              tab.linkedBrowser.contentWindow.history.pushState(null, url, url);
            }
            catch (ex) {}
            tab.linkedBrowser.loadURI(url);
          }, 100);
        }
        else {
          tab.linkedBrowser.loadURI(url);
          if (aPop) {
            BMSVC.moveItem(
              aId, bookstack.stack.getShelf(), BMSVC.DEFAULT_INDEX);
          }
        }
      }
      catch (exLoad) {}
      return true;
    },

    // Hopefully the fastest way to get this info as it's mostly superficial
    // Add 1 to accommodate the offset (used to not need to because of trash).
    getItemCount: function () {
      return BMSVC.getItemIndex(
        BMSVC.getIdForItemAt(bookstack.stack.getShelf(), -1)) + 1;
    },

    // Switch remove on view to the opposite of its current setting.
    toggleRItems: function () {
      bookstack.pref.STACK_RITEMS.persist(!bookstack.pref.STACK_RITEMS.value());
    },

    // Switch clear on exit to the opposite of its current setting.
    toggleCExit: function () {
      bookstack.pref.STACK_CEXIT.persist(!bookstack.pref.STACK_CEXIT.value());
    },

    // Switch middle-click on links adds to stack to the opposite of its
    // current setting.
    toggleMiddl: function () {
      bookstack.pref.STACK_MIDCLICK.persist(
        !bookstack.pref.STACK_MIDCLICK.value());
    },

    pushLivemarkFolder: function(aLivemark, aWhere, aPop) {
      var shelf = this.getShelf(),
          containerNode = PlacesUtils.getFolderContents(aLivemark.id, false,
                                                        false).root,
          children,
          i;

      if (!containerNode.containerOpen) {
        // Should never happen (PU.getFolderContents guarantees the root open)
        return;
      }

      children = aLivemark.getNodesForContainer(containerNode);

      for (i = 0; i < children.length; i += 1) {
        let child = children[i];
        let uri = child.uri;
        let title = child.title;
        BMSVC.insertBookmark(shelf, bookstack.serv.getURI(uri), 0,
                             title || uri);
      }

      containerNode.containerOpen = false;

      if (i > 0) {
        BMSVC.moveItem(aLivemark.id, shelf, BMSVC.DEFAULT_INDEX);
        let itemId = BMSVC.getIdForItemAt(shelf, 0);
        bookstack.stack.popItem(itemId, aWhere, aPop);
      }
      else {
        aLivemark.reload();
      }
    },

    // For a subfolder or livemark, duplicate the items in it recursively,
    // adding them at the top of the stack in their original order
    pushFolder: function (aId, aAdded) {
      var shelf = this.getShelf(),
          folderNode,
          i,
          childNode,
          type,
          childId;
      folderNode = PlacesUtils.getFolderContents(aId, false, false).root;
      if (typeof aAdded === "undefined") {
        aAdded = 0;
      }
      for (i = 0; i < folderNode.childCount; i += 1) {
        childNode = folderNode.getChild(i);
        type = childNode.type;
        childId = childNode.itemId;
        if (type === folderNode.RESULT_TYPE_FOLDER ||
            type === folderNode.RESULT_TYPE_REMOTE_CONTAINER) {
          aAdded = this.pushFolder(childId, aAdded);
        }
        else if (type === folderNode.RESULT_TYPE_URI) {
          BMSVC.insertBookmark(
            shelf,
            BMSVC.getBookmarkURI(childId),
            aAdded,
            BMSVC.getItemTitle(childId));
          aAdded += 1;
        }
        else if (type === folderNode.RESULT_TYPE_SEPARATOR) {
          BMSVC.insertSeparator(shelf, aAdded);
          aAdded += 1;
        }
      }
      folderNode.containerOpen = false;
      return aAdded;
    },

    // Helper to pop up the confirm() question.
    confirmClear: function () {
      return bookstack.serv.getWindow().confirm(this.getString("conftext"));
    },

    // Remove all items from the stack EXCEPT any subfolders and contents.
    // |conf| determines whether a confirmation will be required.
    onClrStack: function (aConf) {
      var win = bookstack.serv.getWindow(),
          shelf = this.getShelf(),
          ritems = bookstack.pref.STACK_RITEMS.value(),
          folderNode,
          i,
          childNode,
          type,
          itemSet = [],
          doBatch,
          closure;
      if (aConf && bookstack.pref.STACK_CCLR.value()) {
        if (!bookstack.stack.confirmClear()) {
          return;
        }
        if (!ritems) {
          bookstack.pref.STACK_RITEMS.persist(true);
        }
      }
      folderNode = PlacesUtils.getFolderContents(shelf, false, false).root;
      for (i = 0; i < folderNode.childCount; i += 1) {
        childNode = folderNode.getChild(i);
        type = childNode.type;
        if (type === folderNode.RESULT_TYPE_URI ||
            type === folderNode.RESULT_TYPE_SEPARATOR) {
          itemSet.push(childNode.itemId);
        }
      }
      folderNode.containerOpen = false;
      doBatch = {
        runBatched: function (aUserData) {
          var closed = aUserData.wrappedJSObject;
          closed.items.forEach(BMSVC.removeItem);
        }
      };
      closure = { items: itemSet };
      closure.wrappedJSObject = closure;
      try {
        BMSVC.runInBatchMode(doBatch, closure);
      }
      catch (ex) {}
      bookstack.pref.STACK_RITEMS.persist(ritems);
    },

    windowWatchObserver: {
      observe: function (aSubject, aTopic, aData) {
        var theWindow = aSubject.QueryInterface(Ci.nsIDOMWindow),
            index = bookstack.stack.windows.indexOf(theWindow);
        if (aTopic === "domwindowclosed" && index !== -1) {
          bookstack.stack.windows.splice(index, 1);
        }
      }
    },

    // Clean up when we're done.
    unload: function () {
      bookstack.stack.unwatchBookmarks();
      if (bookstack.pref.STACK_CEXIT.value()) {
        this.onClrStack(true);
      }
      bookstack.serv.WindowWatcher().unregisterNotification(
        bookstack.stack.windowWatchObserver);
    }
  };
}());

